List table
Allow users to easily scan and quickly access key data organized in logical, hierarchical patterns
#Examples
Composition:
- Background: A subtle background color can visually separate the list table from surrounding content. Use a neutral color that doesn't compete with the content.
- Item Count (Optional): Display the total number of items in the list, especially for filtered or large datasets. Add context when applicable (e.g., "Filtered by: Active").
- Sorting Indicator (Optional): Clearly indicate the active sorting criteria (e.g., "Sorted by Name (Ascending)"). This helps users understand how the list is organized.
- Rows and Cells (Optional): Rows represent individual data items, while cells hold specific pieces of information within a row.
#Basic usage
Use Cases:
- Surface essential information quickly (e.g., site names, scores, policy summaries).
- Offer an overview of data, acting as an entry point to more details. (e.g., Core wins).
- Enable users to scan for patterns and insights.
Best Practices:
- Focus on the most important data points, keeping information density low to avoid overwhelming users.
- Consider embedding list tables within cards or other primary content areas to provide context.
- Keep additional actions (shortcuts, CTAs) to a maximum of two per row to maintain visual clarity.
Dish | Calories | Protein |
|---|---|---|
Beef Stir-Fry | 450 calories | 25 g protein |
Grilled Salmon | 500 calories | 30 g protein |
Veggie Lasagne | 350 calories | 12 g protein |
<ListTable
items={sortItems(items, { property: "dish", direction: "asc" })}
columns={[
{
header: { content: "Dish" },
render: (item) => <div>{item.dish}</div>,
options: { isKeyColumn: true },
},
{ header: { content: "Calories" }, render: (item) => <div>{item.calories}</div> },
{ header: { content: "Protein" }, render: (item) => <div>{item.protein}</div> },
]}
loading={false}
caption="Some table data in a list"
{...translations}
/>#Usage with pagination
Use for large datasets. Provide clear controls and indicate the total number of pages. Consider offering options to adjust items per page.
Dish | Calories | Protein |
|---|---|---|
Beef Stir-Fry | 450 calories | 25 g protein |
Grilled Salmon | 500 calories | 30 g protein |
const [page, setPage] = useState(1);
const [pageSize, setPageSize] = useState(2);
const visibleItems = sortItems(items, { property: "dish", direction: "asc" }).slice(
(page - 1) * pageSize,
page * pageSize
);
return (
<ListTable
items={visibleItems}
columns={[
{
header: { content: "Dish" },
render: (item) => <div>{item.dish}</div>,
options: { isKeyColumn: true },
},
{ header: { content: "Calories" }, render: (item) => <div>{item.calories}</div> },
{ header: { content: "Protein" }, render: (item) => <div>{item.protein}</div> },
]}
loading={false}
caption="Some table data in a list"
pagination={{
total: items.length,
count: pageSize,
page: page,
setPage: setPage,
pageSize: pageSize,
setPageSize: setPageSize,
cancelLabel: "Cancel",
confirmLabel: "Confirm",
firstLabel: "First",
prevLabel: "Previous",
nextLabel: "Next",
lastLabel: "Last",
pagingInfoLabel: (startIdx: number, endIdx: number, total: number) =>
`${startIdx} - ${endIdx} of ${total} items`,
pageLabel: "Page",
pageXofYLabel: (current: number, total: number) => `Page ${current} of ${total}`,
pageSizeSelectionLabel: (pageSize: number) => `${pageSize} items`,
pageSizeSelectorPrefix: "Show",
pageSizeSelectorPostfix: "per page",
pageSizeLabel: "Items per page",
defaultError: "Default pagination error",
wholeNumberError: "Must be a whole number",
outOfBoundsError: (total: number) => `Enter a number between 1 and ${total}`,
}}
{...translations}
/>
);#Usage with load more buttons
Alternative to pagination when space is limited or the total item count is unknown. Ensure the button is visible and provide loading feedback.
Dish | Calories | Protein |
|---|---|---|
Beef Stir-Fry | 450 calories | 25 g protein |
<ListTable
sort={{ property: "dish", direction: "asc" }}
items={sortItems(items, { property: "dish", direction: "asc" })}
columns={[
{
header: { content: "Dish", property: "dish" },
render: (item) => <div>{item.dish}</div>,
options: { isKeyColumn: true },
},
{ header: { content: "Calories" }, render: (item) => <div>{item.calories}</div> },
{ header: { content: "Protein" }, render: (item) => <div>{item.protein}</div> },
]}
loading={false}
caption="Some table data in a list"
loadMoreCount={1}
showLoadAll
{...translations}
/>#Usage with sorting
Allow users to reorder the list. Use a Select for options and indicate the current sorting state.
When using sorted lists, you must provide the sortSelect and sort properties as well as the colums[].header.property value of sortable columns.
Dish | Calories | Protein |
|---|---|---|
Beef Stir-Fry | 450 calories | 25 g protein |
Grilled Salmon | 500 calories | 30 g protein |
Veggie Lasagne | 350 calories | 12 g protein |
const [sort, setSort] = useState(sortOptions[0]);
return (
<ListTable
items={sortItems(items, sort)}
columns={[
{
header: { content: "Dish", property: "dish" },
render: (item) => <div>{item.dish}</div>,
options: { isKeyColumn: true },
},
{ header: { content: "Calories" }, render: (item) => <div>{item.calories}</div> },
{ header: { content: "Protein" }, render: (item) => <div>{item.protein}</div> },
]}
loading={false}
sort={sort}
sortSelect={sortSelectPropsHelper(
"Some a11y label",
sortOptions,
sort,
(property, direction) =>
setSort(
sortOptions.find((x) => x.property === property && x.direction === direction) ||
sortOptions[0]
)
)}
caption="Some table data in a list"
{...translations}
/>
);#Usage with table toolbar
Provide a centralized space for actions that apply to the entire table (filtering, bulk actions, customization). Keep it uncluttered and visually distinct.
Dish | Calories | Protein |
|---|---|---|
Beef Stir-Fry | 450 calories | 25 g protein |
Grilled Salmon | 500 calories | 30 g protein |
Veggie Lasagne | 350 calories | 12 g protein |
type Calories = { id: number; name: string };
const caloriesAmount: Calories[] = [
{ id: 1, name: "All calories" },
{ id: 2, name: "Less than 500 calories" },
{ id: 3, name: "Less than 400 calories" },
];
const [calories, setCalories] = useState<Calories | undefined>(caloriesAmount[0]);
const onChange = (newValue: Calories | undefined) => {
console.log("Filter changed, calling API with new calories count", newValue);
setCalories(newValue);
};
const [filterButton, activeFilters] = useSingleFilter(calories, onChange, {
label: "Calories",
stringify: (calories) => calories?.name,
items: caloriesAmount.map((calories) => ({ title: calories.name, value: calories })),
compareFn: (a, b) => a.id === b.id,
defaultOption: caloriesAmount[0],
});
const sort: SortField<(typeof items)[0]> = {
property: "dish",
direction: "asc",
};
const displayedItems =
calories?.id === 1
? items
: items.filter((x) => {
const caloriesMatch = x.calories.match(/\d+/);
if (!caloriesMatch) return false;
const caloriesValue = Number(caloriesMatch[0]);
return calories?.id === 2 ? caloriesValue < 500 : caloriesValue < 400;
});
return (
<>
<TableToolbar filter={filterButton} activeFilters={activeFilters} />
<ListTable
items={sortItems(displayedItems, { property: sort.property, direction: sort.direction })}
columns={[
{
header: { content: "Dish", property: "dish" },
render: (displayedItems) => <div>{displayedItems.dish}</div>,
options: { isKeyColumn: true },
},
{
header: { content: "Calories" },
render: (displayedItems) => <div>{displayedItems.calories}</div>,
},
{
header: { content: "Protein" },
render: (displayedItems) => <div>{displayedItems.protein}</div>,
},
]}
sort={sort}
loading={false}
caption="Some table data in a list"
{...translations}
/>
</>
);#Usage without filter, sort or count header
For simple, static lists where these features aren't needed. Ensure the list is self-explanatory and visually organized. Consider a card header or title for context.
Dish | Calories | Protein |
|---|---|---|
Beef Stir-Fry | 450 calories | 25 g protein |
Grilled Salmon | 500 calories | 30 g protein |
Veggie Lasagne | 350 calories | 12 g protein |
return (
<ListTable
items={sortItems(items, { property: "dish", direction: "asc" })}
columns={[
{
header: { content: "Dish" },
render: (item) => <div>{item.dish}</div>,
options: { isKeyColumn: true },
},
{ header: { content: "Calories" }, render: (item) => <div>{item.calories}</div> },
{ header: { content: "Protein" }, render: (item) => <div>{item.protein}</div> },
]}
loading={false}
caption="Some table data in a list"
loadMoreLabel={(count: number) => `Load ${count} more`}
loadAllLabel="Load all"
// leave out the "itemsFoundLabel", "sortByLabel" & "sortedByLabel" props to avoid the header
/>
);#Loading state
Use a Spinner and/or brief message to indicate data is being fetched.
Best Practices:
- Use a Spinner component to indicate the loading state.
- Place the loading indicator in the center of the list table area.
- Consider adding a brief message explaining the delay.